/*
 * Copyright (C) 2014 Nathan Templon
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package graph;

import static util.Util.doubleEquals;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;

import log.Logger;
import function.Function.Domain;
import function.Function.FuncPoint;
import function.Function.Range;
import function.Function.Variable;
import function.SingleVariableFunction;

/**
 *
 * @author Nathan Templon
 */
public class Plot2D extends JPanel {
	
	// -------------------------------------------------------------------------------------------------
    // ---------------------------  State Variables ----------------------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    private final Collection<Collection<FuncPoint>> pointCollections; // The sets of points to be drawn
    private final Map<SingleVariableFunction, Collection<FuncPoint>> pointFuncMap; // The functions to be drawn as point functions
    private final Map<SingleVariableFunction, ArrayList<FuncPoint>> lineFuncMap; // The function to be drawn as line functions
    
    private final DecimalFormat sciNotFormat; // The format object that will format numbers for labels in scientific notation
    private DecimalFormat standNotFormat; // The format object that will format numbers for labels in standard notation
    
    private Color lineColor = Color.BLACK;
    private Color pointColor = Color.BLACK;
    
    private final int width = 800;
    private final int height = 400;
    private int padding = 10;
    private int leftLabelPadding = 50;
    private int bottomLabelPadding = 25;
    private int rightSidePadding = 5;
    private int topPadding = 15;
    private int graphPixelWidth;
    private int graphPixelHeight;
    private int smallFontSize;
    private int bigFontSize;
    private int titleFontSize;
    
    private Font standardFont;
    private Font boldFont;
    private Font bigFont;
    private Font titleFont;
    
    private FontMetrics standardMetrics;
    private FontMetrics boldMetrics;
    private FontMetrics bigMetrics;
    private FontMetrics titleMetrics;
    
    private Map<Integer, Double> xGridlinePixels = new HashMap<>();
    private Map<Integer, Double> yGridlinePixels = new HashMap<>();
    
    private final Color gridColor = new Color(200, 200, 200, 200);
    
    private double xMin = -1.0;
    private double xMax = 10;
    private double yMin = -1.0;
    private double yMax = 7.0;
    private int xOfYAxis = 0;
    private int yOfXAxis = 0;
    
    private double scrollOutSens = 0.05;
    private double scrollInSens = 1.0 - (1.0 / (1.0 + scrollOutSens));
    
    private String xAxisTitle = "Horizontal Axis";
    private String yAxisTitle = "Vertical Axis";
    private String title = "TITLE";
    
    private double xWidth;
    private double yWidth;
    
    private double pixelsPerUnitX;
    private double pixelsPerUnitY;
    
    private int numXGridlines = 5;
    private int numYGridlines = 5;
    
    private int xTicksPerGridline = 5;
    private int yTicksPerGridline = 5;
    private int tickSize = 5;
    
    private int leftX;
    private int rightX;
    private int topY;
    private int bottomY;
    private int centerX;
    
    private int pointRadius = 4;
    
    private int numFunctionPoints = 500;
    
    private boolean drawAxes = false;
    private boolean drawGridlines = false;
    private boolean drawTicks = false;
    private boolean drawLabels = true;
    private boolean drawXAxisTitle = true;
    private boolean drawYAxisTitle = true;
    private boolean drawGraphTitle = true;
    
    private Logger logger;
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Constructors  ------------------------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    /**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	/**
     * The default constructor, which creates a new, blank Plot2D
     */
    public Plot2D() {
        logger = Logger.getDefaultLogger();
        
        sciNotFormat = new DecimalFormat("0.0000E0");
        standNotFormat = new DecimalFormat("##0.####");
                
        pointCollections = new ArrayList<>();
        pointFuncMap = new HashMap<>();
        lineFuncMap = new HashMap<>();
        
        GraphicsMouseAdapter adapt = new GraphicsMouseAdapter(this);
        this.addMouseListener(adapt);
        this.addMouseWheelListener(adapt);
        this.addMouseMotionListener(adapt);
    }
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Painting Methods  --------------------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    /**
     * Paints the component
     * @param g the graphics object that can paint this component
     */
    @Override
    protected void paintComponent(Graphics g) {
        // Call the super paintComponent
        super.paintComponent(g);
        
        // Calculate paddings and such
        calcMetrics();
        
        // Customize and configure our graphics object
        Graphics2D graphics = (Graphics2D)g;
        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        
        // Instantiate our fonts and font metrics
        initFonts(graphics);
        graphics.setFont(standardFont);
        
        // Draw a white background
        drawBackground(graphics);
        
        // Draw Grid Lines
        if (drawGridlines) {
        	drawGridlines(graphics);
        }
        
        // Create x and y axes 
        if (drawAxes) {
        	drawAxes(graphics);
        }
        
        // Draw the Labels for the light lines
        if (drawLabels) {
        	drawLabels(graphics);
        }
        
        // Draw the Axis titles
        drawAxisTitles(graphics);
        
        // Draw graph title
        if (drawGraphTitle) {
        	drawGraphTitle(graphics);
        }
        
        // Draw ticks
        if (drawTicks) {
        	drawTicks(graphics);
        }
        
        // Draw the functions
        drawFunctions(graphics);
    }
    
    
    /**
     * Gives the component's preferred size
     * @return Returns the component's preferred size
     */
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(width, height);
    }
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Painting Helpers  --------------------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    private String getFormatedForLabel(double d) {
    	// Calculate the power of the number
    	int power = (int)Math.round(Math.floor(Math.log10(Math.abs(d))));
    	
    	if (power == 4) {
    		standNotFormat = new DecimalFormat("00000");
    		return standNotFormat.format(d);
    	}
    	else if (0 <= power && power <= 3) {
    		String formatString = "";
    		for (int index = 0; index < power + 1; index++) {
    			formatString += "0";
    		}
    		formatString += ".";
    		for (int index = power + 1; index < 5; index++) {
    			formatString += "0";
    		}
    		standNotFormat = new DecimalFormat(formatString);
    		return standNotFormat.format(d);
    	}
    	else if (power >= -2) {
    		String formatString = "0.";
    		for (int i = 0; i < (-1 * power) - 1; i++) {
    			formatString += "0";
    		}
    		formatString += "00000";
    		standNotFormat = new DecimalFormat(formatString);
    		return standNotFormat.format(d);
    	}
    	
        return sciNotFormat.format(d);
    }
    
    
    private void calcMetrics() {
        // Calculate font sizes
        bigFontSize = (int)(getWidth() / 100.0 ) + 4;
        smallFontSize = (int)(bigFontSize * 0.2) + 8;
        titleFontSize = (int)(1.2 * bigFontSize);
        
        // Calculate padding
        padding = 15 + (int)(getWidth() * 0.004);
        rightSidePadding = 5 + (int)(getWidth() * 0.001);
        bottomLabelPadding = (int)(1.4 * bigFontSize) + 10;
        leftLabelPadding = (int)(1.2 * bigFontSize) + 45;
        topPadding = (int)(1.9 * titleFontSize);
        
        // Calculate the position of the four corners
        leftX = padding + leftLabelPadding;
        rightX = getWidth() - (padding + rightSidePadding);
        bottomY = getHeight() - (padding + bottomLabelPadding);
        topY = topPadding;
        
        // Calculate the height of the graph in pixels
        graphPixelWidth = rightX - leftX;
        graphPixelHeight = bottomY - topY;
        
        // Calculate the width and height of the graph in units
        xWidth = xMax - xMin;
        yWidth = yMax - yMin;
        
        // Calculate how many pixels are in one unit of each scales
        pixelsPerUnitX = ((double)graphPixelWidth)/xWidth;
        pixelsPerUnitY = ((double)graphPixelHeight)/yWidth;
        
        // Decide how large to make the tick marks
        tickSize = (int)Math.round((double)Math.min(graphPixelWidth, graphPixelHeight) * 0.01);
        
        // Calculate where the Axes are
        xOfYAxis = leftX + Math.max(0, (int)Math.round(pixelsPerUnitX * -1 * xMin));
        xOfYAxis = Math.min(xOfYAxis, rightX);
        yOfXAxis = bottomY - Math.max(0, (int)Math.round(pixelsPerUnitY * -1 * yMin));
        yOfXAxis = Math.max(yOfXAxis, topY);
        
        
        // Calculate x grid lines
        double xGridlineStep = ((double)graphPixelWidth) / ((double)(numXGridlines + 1));
        xGridlinePixels = new HashMap<>();
        // Y Axis is off the left side of the screen
        if (xMin >= 0) {
        	int xGridlinePos0 = -1 * (int)Math.round(xMin * pixelsPerUnitX) + leftX;
        	int xGridlinePos = xGridlinePos0;
        	int numStepsTaken = 0;
        	while (xGridlinePos < leftX) {
        		numStepsTaken++;
        		xGridlinePos = xGridlinePos0 + (int)Math.round(numStepsTaken * xGridlineStep);
        	}
        	while (xGridlinePos < rightX) {
//        		graphics.drawLine(xGridlinePos, topY, xGridlinePos, bottomY);
        		// Calculate exact graph value to make labels more accurate
        		double graphVal = ((numStepsTaken * xGridlineStep) / pixelsPerUnitX);
        		xGridlinePixels.put(xGridlinePos, graphVal);
        		numStepsTaken++;
        		xGridlinePos = xGridlinePos0 + (int)Math.round(numStepsTaken * xGridlineStep);
        	}
        }
        else {
        	// Y Axis is somewhere on the screen
        	if (xMax >= 0) {
        		int xGridlinePos0 = xOfYAxis;
        		int xGridlinePos = xGridlinePos0;
        		int numStepsTaken = 0;
        		while (xGridlinePos > leftX) {
        			numStepsTaken--;
        			xGridlinePos = xGridlinePos0 + (int)Math.round(xGridlineStep * numStepsTaken);
        		}
        		numStepsTaken++;
        		xGridlinePos = xGridlinePos0 + (int)Math.round(xGridlineStep * numStepsTaken);
        		while (xGridlinePos < rightX) {
//        			graphics.drawLine(xGridlinePos, topY, xGridlinePos, bottomY);
        			double graphVal = ((numStepsTaken * xGridlineStep) / pixelsPerUnitX);
        			xGridlinePixels.put(xGridlinePos, graphVal);
        			numStepsTaken++;
        			xGridlinePos = xGridlinePos0 + (int)Math.round(xGridlineStep * numStepsTaken);
        		}
        	}
        	// Y Axis is off to the right of the screen
        	else {
        		int xGridlinePos0 = -1 * (int)Math.round(xMax * pixelsPerUnitX) + rightX;
        		int xGridlinePos = xGridlinePos0;
        		int numStepsTaken = 0;
        		while (xGridlinePos > rightX) {
        			numStepsTaken--;
        			xGridlinePos = xGridlinePos0 + (int)Math.round(xGridlineStep * numStepsTaken);
        		}
        		while(xGridlinePos > leftX) {
//        			graphics.drawLine(xGridlinePos, topY, xGridlinePos, bottomY);
        			double graphVal = ((numStepsTaken * xGridlineStep) / pixelsPerUnitX);
        			xGridlinePixels.put(xGridlinePos, graphVal);
        			xGridlinePos -= (int)xGridlineStep;
        			numStepsTaken--;
        			xGridlinePos = xGridlinePos0 + (int)Math.round(xGridlineStep * numStepsTaken);
        		}
        	}
        }
        
        
        // Calculate y grid lines
        double yGridlineStep = ((double)graphPixelHeight) / ((double)(numYGridlines + 1));
        yGridlinePixels = new HashMap<>();
        // X Axis is off the bottom of the screen
        if (yMin >= 0) {
        	int yGridlinePos0 = (int)Math.round(yMin * pixelsPerUnitY) + bottomY;
        	int yGridlinePos = yGridlinePos0;
        	int numStepsTaken = 0;
        	while (yGridlinePos > bottomY) {
        		numStepsTaken--;
        		yGridlinePos = yGridlinePos0 + (int)Math.round(numStepsTaken * yGridlineStep);
        	}
        	while (yGridlinePos > topY) {
//        		graphics.drawLine(leftX, yGridlinePos, rightX, yGridlinePos);
        		double yVal = (-1.0 * (numStepsTaken * yGridlineStep) / pixelsPerUnitY);
        		yGridlinePixels.put(yGridlinePos, yVal);
        		numStepsTaken--;
        		yGridlinePos = yGridlinePos0 + (int)Math.round(numStepsTaken * yGridlineStep);
        	}
        }
        else {
        	// X Axis is somewhere on the screen
        	if (yMax >= 0) {
        		int yGridlinePos0 = yOfXAxis;
        		int yGridlinePos = yGridlinePos0;
        		int numStepsTaken = 0;
        		while (yGridlinePos > topY) {
        			numStepsTaken--;
        			yGridlinePos = yGridlinePos0 + (int)Math.round(yGridlineStep * numStepsTaken);
        		}
        		numStepsTaken++;
        		yGridlinePos = yGridlinePos0 + (int)Math.round(yGridlineStep * numStepsTaken);
        		while (yGridlinePos < bottomY) {
//        			graphics.drawLine(leftX, yGridlinePos, rightX, yGridlinePos);
        			double yVal = (-1.0 * (numStepsTaken * yGridlineStep) / pixelsPerUnitY);
        			yGridlinePixels.put(yGridlinePos, yVal);
            		numStepsTaken++;
            		yGridlinePos = yGridlinePos0 + (int)Math.round(yGridlineStep * numStepsTaken);
        		}
        	}
        	// X Axis is off the top of the screen
        	else {
        		int yGridlinePos0 = (int)Math.round(yMax * pixelsPerUnitY) + topY;
        		int yGridlinePos = yGridlinePos0;
        		int numStepsTaken = 0;
        		while (yGridlinePos < topY) {
        			numStepsTaken++;
        			yGridlinePos = yGridlinePos0 + (int)Math.round(yGridlineStep * numStepsTaken);
        		}
        		while (yGridlinePos < bottomY) {
//        			graphics.drawLine(leftX, yGridlinePos, rightX, yGridlinePos);
        			double yVal = (-1.0 * (numStepsTaken * yGridlineStep) / pixelsPerUnitY);
        			yGridlinePixels.put(yGridlinePos, yVal);
            		numStepsTaken++;
        			yGridlinePos = yGridlinePos0 + (int)Math.round(yGridlineStep * numStepsTaken);
        		}
        	}
        }
    }
    
    
    private void initFonts(Graphics2D graphics) {
        standardFont = new Font("Default", Font.PLAIN, smallFontSize);
        boldFont = standardFont.deriveFont(Font.BOLD);
        bigFont = new Font(standardFont.getFontName(), Font.PLAIN, bigFontSize);
        titleFont = new Font(standardFont.getFontName(), Font.BOLD, titleFontSize);
        
        standardMetrics = graphics.getFontMetrics(standardFont);
        boldMetrics = graphics.getFontMetrics(boldFont);
        bigMetrics = graphics.getFontMetrics(bigFont);
        titleMetrics = graphics.getFontMetrics(titleFont);
    }
    
    
    private void drawBackground(Graphics2D graphics) {
        Color oldColor = graphics.getColor();
        graphics.setColor(Color.WHITE);
        graphics.fillRect(leftX, topY, graphPixelWidth, graphPixelHeight);
        graphics.setColor(oldColor);
    }
    
    
    private void drawGridlines(Graphics2D graphics) {
    	// Prepare to draw grid lines
        graphics.setColor(gridColor);
        
        // Draw x grid lines
        for (Integer xPos : xGridlinePixels.keySet()) {
        	graphics.drawLine(xPos, topY, xPos, bottomY);
        }
        
        
        // Draw y grid lines
        for (Integer yPos : yGridlinePixels.keySet()) {
        	graphics.drawLine(leftX, yPos, rightX, yPos);
        }
        
        // Return the graphics to normal colors
        graphics.setColor(Color.BLACK);
    }
    
    
    private void drawAxes(Graphics2D graphics) {
        // create x and y axes 
        Stroke oldStroke = graphics.getStroke();
        graphics.setStroke(new BasicStroke(2));
        graphics.drawLine(xOfYAxis, bottomY, xOfYAxis, topY);
        graphics.drawLine(leftX, yOfXAxis, rightX, yOfXAxis);
        graphics.setStroke(oldStroke);
    }
    
    
    private void drawLabels(Graphics2D graphics) {
        // Set font to bold
        graphics.setFont(boldFont);
        
    	// Draw x labels
        for (int xPos : xGridlinePixels.keySet()) {
        	double xValueAtLine = xGridlinePixels.get(xPos);
        	String xLabel = getFormatedForLabel(xValueAtLine);
        	int labelWidth = boldMetrics.stringWidth(xLabel);
        	graphics.drawString(xLabel, xPos - labelWidth / 2, bottomY + boldMetrics.getHeight() + 3);
        }
        
        // Draw y labels
        for (int yPos : yGridlinePixels.keySet()) {
        	double yValueAtLine = yGridlinePixels.get(yPos);
        	String yLabel = getFormatedForLabel(yValueAtLine);
        	int labelWidth = boldMetrics.stringWidth(yLabel);
        	graphics.drawString(yLabel, leftX - labelWidth - 5, yPos + (boldMetrics.getHeight() / 2) - 3);
        }
        
        graphics.setFont(standardFont);
    }
    
    
    private void drawAxisTitles(Graphics2D graphics) {
    	// Set the font for the axis titles
        graphics.setFont(bigFont);
        
        //Draw x Axis Title
        if (drawXAxisTitle) {
        	centerX = (int)Math.round(((double)(rightX - leftX)) / 2.0) + leftX;
        	int xTitleLength = bigMetrics.stringWidth(xAxisTitle);
        	graphics.drawString(xAxisTitle, centerX - xTitleLength /  2, bottomY +  bigMetrics.getHeight() + boldMetrics.getHeight() + 5);
        }
        
        // Draw y Axis Title
        if (drawYAxisTitle) {
        	int centerY = (int)Math.round(((double)(bottomY - topY)) / 2.0) + topY;
        	int yTitleLength = standardMetrics.stringWidth(yAxisTitle);
        	AffineTransform origTrans = graphics.getTransform();
        	graphics.rotate(-1 * Math.PI / 2.0);
        	graphics.drawString(yAxisTitle, -1 * centerY - (yTitleLength / 2), leftX - boldMetrics.stringWidth("0.0000E0") - 15);
        	graphics.setTransform(origTrans);
        }
        
        graphics.setFont(standardFont);
    }
    
    
    
    private void drawGraphTitle(Graphics2D graphics) {
    	// Draw the graph's title
        titleMetrics = graphics.getFontMetrics(titleFont);
        graphics.setFont(titleFont);
        graphics.drawString(title, centerX - titleMetrics.stringWidth(title) /  2, titleMetrics.getHeight());
        
        // Return the font to normal
        graphics.setFont(standardFont);
    }
    
    
    
    private void drawTicks(Graphics2D graphics) {
    	// Draw x tick marks
        int numXTicks = ((numXGridlines + 1) * xTicksPerGridline) - 1;
        double xTickStep = ((double)graphPixelWidth) / ((double)(numXTicks + 1));
        int minX = 0;
        int maxX = 0;
        boolean minMaxSet = false;
        
        // Find the maximum and minimum grid line positions
        for(int tickX : xGridlinePixels.keySet()) {
        	if (!minMaxSet) {
        		minX = tickX;
        		maxX = tickX;
        		minMaxSet = true;
        	}
        	minX = Math.min(minX, tickX);
        	maxX = Math.max(maxX, tickX);
        }
        
        // Draw the ticks for all but before the minimum and after the maximum x of a grid line
        for(int tickX : xGridlinePixels.keySet()) {
            graphics.drawLine(tickX, yOfXAxis, tickX, yOfXAxis - tickSize);
            if (tickX != maxX) {
            	for (int index = 1; index < xTicksPerGridline; index++) {
            		graphics.drawLine(tickX + (int)(xTickStep * index), yOfXAxis, tickX + (int)(xTickStep * index), yOfXAxis - tickSize);
            	}
            }
        }
        
        // Draw ticks after the maximum X
        for (int tickX = maxX; tickX < rightX; tickX += xTickStep) {
        	graphics.drawLine(tickX, yOfXAxis, tickX, yOfXAxis - tickSize);
        }
        
        // Draw ticks before the minimum X
        for (int tickX = minX; tickX > leftX; tickX -= xTickStep) {
        	graphics.drawLine(tickX, yOfXAxis, tickX, yOfXAxis - tickSize);
        }
        
        
        
        // Draw y tick marks
        int numYTicks = ((numYGridlines + 1) * yTicksPerGridline) - 1;
        double yTickStep = ((double)graphPixelHeight) / ((double)(numYTicks + 1));
        int minY = 0;
        int maxY = 0;
        minMaxSet = false;
        
        // Find the maximum and minimum grid line positions
        for(int tickY : yGridlinePixels.keySet()) {
        	if (!minMaxSet) {
        		minY = tickY;
        		maxY = tickY;
        		minMaxSet = true;
        	}
        	minY = Math.min(minY, tickY);
        	maxY = Math.max(maxY, tickY);
        }
        
        // Draw the ticks for all but before the minimum and after the maximum y of a grid line
        for (int tickY : yGridlinePixels.keySet()) {
        	graphics.drawLine(xOfYAxis, tickY, xOfYAxis + tickSize, tickY);
        	if (tickY != maxY) {
        		for (int index = 1; index < yTicksPerGridline; index++) {
        			graphics.drawLine(xOfYAxis, tickY + (int)(index * yTickStep), xOfYAxis + tickSize, tickY + (int)(index * yTickStep));
        		}
        	}
        }
        
        // Draw the ticks for the maximum Y
        for (int tickY = maxY; tickY < bottomY; tickY += yTickStep) {
        	graphics.drawLine(xOfYAxis, tickY, xOfYAxis + tickSize, tickY);
        }
        
        // Draw the ticks for the maximum Y
        for (int tickY = minY; tickY > topY; tickY -= yTickStep) {
        	graphics.drawLine(xOfYAxis, tickY, xOfYAxis + tickSize, tickY);
        }
    }
    
    
    
    private void drawFunctions(Graphics2D graphics) {
    	// Draw the points
        Color oldColor = graphics.getColor();
        graphics.setColor(this.pointColor);
        int shiftToCenter = (int)((double)pointRadius/2.0);
        for(Collection<FuncPoint> points : pointCollections) {
            for(FuncPoint point : points) {
                if (point.x() > xMax || point.y() > yMax || point.x() < xMin || point.y() < yMin) {
                    continue;
                }
                
                double relX = point.x() - xMin;
                double relY = point.y() - yMin;
                
                int pointX = leftX + (int)Math.round(pixelsPerUnitX * relX) - shiftToCenter;
                int pointY = bottomY - (int)Math.round(pixelsPerUnitY * relY) - shiftToCenter;
                
                graphics.fillOval(pointX, pointY, pointRadius, pointRadius);
            }
        }
        
        // Draw the point functions
        for(SingleVariableFunction func : pointFuncMap.keySet()) {
            Collection<FuncPoint> points = pointFuncMap.get(func);
            for(FuncPoint point : points) {
                if (point.x() > xMax || point.y() > yMax || point.x() < xMin || point.y() < yMin) {
                    continue;
                }
                
                double relX = point.x() - xMin;
                double relY = point.y() - yMin;
                
                int pointX = leftX + (int)Math.round(pixelsPerUnitX * relX) - shiftToCenter;
                int pointY = bottomY - (int)Math.round(pixelsPerUnitY * relY) - shiftToCenter;
                
                graphics.fillOval(pointX, pointY, pointRadius, pointRadius);
            }
        }
        
        // Draw the line functions
        graphics.setColor(lineColor);
        for(SingleVariableFunction func : lineFuncMap.keySet()) {
            ArrayList<FuncPoint> points = lineFuncMap.get(func);
            int index = 0;
            for(FuncPoint point : points) {
                
                if (index >= points.size() - 1) {
                    index++;
                    continue;
                }
                
                FuncPoint point2 = points.get(index + 1);
                
                if (point.x() > xMax || point.y() > yMax || point.x() < xMin || point.y() < yMin) {
                    if (!(point2.x() > xMax || point2.y() > yMax || point2.x() < xMin || point2.y() < yMin)) {
                        // Draw line from this point to point2, but only the part that is inside of the graph
                        // This point is not on the graph.
                        double slope = (point2.y() - point.y()) / (point2.x() - point.x());
                        double xAtYMax = point.x() + ((yMax - point.y()) / slope);
                        
                        // Then the line from this point to point 2 hits the top of the graph
                        if (point.x() < xAtYMax && point2.x() > xAtYMax) {
                            int pointX = leftX + (int)Math.round(pixelsPerUnitX * (xAtYMax - xMin));
                            int pointY = topY;
                            
                            double relX2 = point2.x() - xMin;
                            double relY2 = point2.y() - yMin;
                            int pointX2 = leftX + (int)Math.round(pixelsPerUnitX * relX2);
                            int pointY2 = bottomY - (int)Math.round(pixelsPerUnitY * relY2);
                            
                            graphics.drawLine(pointX, pointY, pointX2, pointY2);
                        }
                        // The line from this point to point 2 hits the bottom of the graph
                        else {
                        	double xAtYMin = point.x() + ((yMin - point.y()) / slope);
                        	int pointX = leftX + (int)Math.round(pixelsPerUnitX * (xAtYMin - xMin));
                        	int pointY = bottomY;
                        	
                        	double relX2 = point2.x() - xMin;
                            double relY2 = point2.y() - yMin;
                            int pointX2 = leftX + (int)Math.round(pixelsPerUnitX * relX2);
                            int pointY2 = bottomY - (int)Math.round(pixelsPerUnitY * relY2);
                            
                            graphics.drawLine(pointX, pointY, pointX2, pointY2);
                        }
                    }
                    index++;
                    continue;
                }
                
                else if (point2.x() > xMax || point2.y() > yMax || point2.x() < xMin || point2.y() < yMin) {
                    // Draw a line from this point to point2, but only the part that is inside of the graph.
                    // This point is on the graph, and point2 is not
                	
                	double slope = (point2.y() - point.y()) / (point2.x() - point.x());
                	double xAtYMax = point.x() + ((yMax - point.y()) / slope);
                	
                	// Then the line from this point to point 2 hits the top of the graph
                    if (point.x() < xAtYMax && point2.x() > xAtYMax) {
                    	int pointX = leftX + (int)Math.round(pixelsPerUnitX * (point.x() - xMin));
                    	int pointY = topY + (int)Math.round(pixelsPerUnitY * (yMax - point.y()));
                    	
                    	int pointX2 = leftX + (int)Math.round(pixelsPerUnitX * (xAtYMax - xMin));
                    	int pointY2 = topY;
                        
                        graphics.drawLine(pointX, pointY, pointX2, pointY2);
                    }
                    // The line from this point to point 2 hits the bottom of the graph
                    else {
                    	double xAtYMin = point.x() + ((yMin - point.y()) / slope);
                    	
                    	int pointX = leftX + (int)Math.round(pixelsPerUnitX * (point.x() - xMin));
                    	int pointY = topY + (int)Math.round(pixelsPerUnitY * (yMax - point.y()));
                    	
                    	int pointX2 = leftX + (int)Math.round(pixelsPerUnitX * (xAtYMin - xMin));
                    	int pointY2 = bottomY;
                        
                        graphics.drawLine(pointX, pointY, pointX2, pointY2);
                    }
                	
                    index++;
                    continue;
                }
                
                double relX = point.x() - xMin;
                double relY = point.y() - yMin;
                double relX2 = point2.x() - xMin;
                double relY2 = point2.y() - yMin;
                
                int pointX = leftX + (int)Math.round(pixelsPerUnitX * relX);
                int pointY = bottomY - (int)Math.round(pixelsPerUnitY * relY);
                int pointX2 = leftX + (int)Math.round(pixelsPerUnitX * relX2);
                int pointY2 = bottomY - (int)Math.round(pixelsPerUnitY * relY2);
                
                graphics.drawLine(pointX, pointY, pointX2, pointY2);
                index++;
            }
        }
        
        graphics.setColor(oldColor);
    }
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Function/Point Assignments -----------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    
    public void addPoints(Collection<FuncPoint> points) {
        this.pointCollections.add(points);
    }
    
    
    public void addFunction(SingleVariableFunction func, FUNC_TYPE type) {
        ArrayList<FuncPoint> points = new ArrayList<>();
        Variable var = func.getVariables()[0];
        
        double step = (func.domain().max() - func.domain().min()) / ((double)numFunctionPoints);
        for(double index = func.domain().min(); index <= func.domain().max(); index += step) {
            var.setValue(index);
            double funcValue = func.getValue();
            points.add(new FuncPoint(index, funcValue));
        }
        
        if (type == FUNC_TYPE.POINT) {
            pointFuncMap.put(func, points);
        }
        else if (type == FUNC_TYPE.LINE) {
            lineFuncMap.put(func, points);
        }
    }
    
    
    public void updateFunctions() {
        Map<SingleVariableFunction, Collection<FuncPoint>> updatedPoints = new HashMap<>();
        for(SingleVariableFunction func : pointFuncMap.keySet()) {
            Variable var = func.getVariables()[0];
            ArrayList<FuncPoint> points = new ArrayList<>();
            
            double min = Math.max(func.domain().min(), xMin);
            double max = Math.min(func.domain().max(), xMax);
            
            double step = (max - min) / ((double)(numFunctionPoints - 1.0));
            double val = min;
            
            if (doubleEquals(min, max) || min > max) {
                continue;
            }
            
            boolean drawnLastPoint = false;
            while (!drawnLastPoint) {
                if (val > max) {
                    val = max;
                    drawnLastPoint = true;
                }
                var.setValue(val);
                double funcValue = func.getValue();
                points.add(new FuncPoint(val, funcValue));
                val += step;
            }
            updatedPoints.put(func, points);
        }
        for (SingleVariableFunction func : updatedPoints.keySet()) {
            pointFuncMap.remove(func);
            pointFuncMap.put(func, updatedPoints.get(func));
        }
        
        Map<SingleVariableFunction, ArrayList<FuncPoint>> updatedLines = new HashMap<>();
        for(SingleVariableFunction func : lineFuncMap.keySet()) {
            Variable var = func.getVariables()[0];
            ArrayList<FuncPoint> points = new ArrayList<>();
            
            double min = Math.max(func.domain().min(), xMin);
            double max = Math.min(func.domain().max(), xMax);
            
            double step = (max - min) / ((double)(numFunctionPoints - 1));
            double val = min;
            
            if (doubleEquals(min, max) || min > max) {
                continue;
            }
            
            boolean drawnLastPoint = false;
            while (!drawnLastPoint) {
                if (val > max) {
                    val = max;
                    drawnLastPoint = true;
                }
                var.setValue(val);
                double funcValue = func.getValue();
                points.add(new FuncPoint(val, funcValue));
                val += step;
            }
            updatedLines.put(func, points);;
        }
        for (SingleVariableFunction func : updatedLines.keySet()) {
            lineFuncMap.remove(func);
            lineFuncMap.put(func, updatedLines.get(func));
        }
    }
    
    
    public void addFunction(SingleVariableFunction func) {
        addFunction(func, FUNC_TYPE.POINT);
    }
    
    
    public void removeFunction(SingleVariableFunction func) {
        pointFuncMap.remove(func);
    }
    
    
    public void removePoints(Collection<FuncPoint> points) {
        if (pointCollections.contains(points)) {
            this.pointCollections.remove(points);
        }
    }
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Property Getters/Setters -------------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    
    public void setDomain(Domain d) {
        this.xMin = d.min();
        this.xMax = d.max();
    }
    
    
    public Domain getDomain() {
        return new Domain(xMin, xMax);
    }
    
    
    public void setRange(Range r) {
        this.yMin = r.min();
        this.yMax = r.max();
    }
    
    
    public Range getRange() {
        return new Range(yMin, yMax);
    }
    
    
    public void setXTicksPerGridline(int ticks) {
        this.xTicksPerGridline = ticks;
    }
    
    
    public int getXTicksPerGridline() {
        return xTicksPerGridline;
    }
    
    
    public void setYTicksPerGridline(int ticks) {
        this.yTicksPerGridline = ticks;
    }
    
    
    public int getYTicksPerGridline() {
        return yTicksPerGridline;
    }
    
    
    public void setPointRadius(int r) {
        this.pointRadius = r;
    }
    
    
    public int getPointRadius() {
        return pointRadius;
    }
    
    
    public void setNumFuncPoints(int points) {
        this.numFunctionPoints = points;
        updateFunctions();
    }
    
    
    public int getNumFuncPoints() {
        return numFunctionPoints;
    }
    
    
    public void setXAxisTitle(String title) {
        this.xAxisTitle = title;
    }
    
    
    public String getXAxisTitle() {
        return xAxisTitle;
    }
    
    
    public void setYAxisTitle(String title) {
        this.yAxisTitle = title;
    }
    
    
    public String getYAxisTitle() {
        return yAxisTitle;
    }
    
    
    public void setLineColor(Color color) {
        this.lineColor = color;
    }
    
    
    public Color getLineColor() {
        return lineColor;
    }
    
    
    public void setPointColor(Color color) {
        this.pointColor = color;
    }
    
    
    public Color getPointColor() {
        return pointColor;
    }
    
    
    
    
    
    public void square() {
    	calcMetrics();
    	double commonPixelsPerUnit = Math.min(pixelsPerUnitX, pixelsPerUnitY);
    	logger.log(commonPixelsPerUnit);
    	
    	double centerX = 0.5 * (xMin + xMax);
    	double centerY = 0.5 * (yMin + yMax);
    	
    	int width = rightX - leftX;
    	int height = bottomY - topY;
    	
    	xMin = centerX - (width / 2.0) / commonPixelsPerUnit;
    	xMax = centerX + (width / 2.0) / commonPixelsPerUnit;
    	yMin = centerY - (height / 2.0) / commonPixelsPerUnit;
    	yMax = centerY + (height / 2.0) / commonPixelsPerUnit;
    	
    	updateFunctions();
    	repaint();
    }
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Adapters and Auxilary GUIs -----------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    
    class GraphRightClickMenu extends JPopupMenu implements ActionListener {
        /**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		private JMenuItem item1;
		private JMenuItem item2;
		private JMenuItem snapSubMenu;
		private JMenuItem snapToXItem;
		private JMenuItem snapToYItem;
		private JMenuItem snapToOriginItem;
		
		private Plot2D plot;
        
        public GraphRightClickMenu(Plot2D plot) {
        	this.plot = plot;
        	
            item1 = new JMenuItem("Item 1");
            item2 = new JMenuItem("Item 2");
            add(item1);
            add(item2);
            
            // Handle "Snap to Axes" functionality
            snapSubMenu = new JMenu("Snap To Axis");
            snapToXItem = new JMenuItem("X Axis");
            snapToXItem.addActionListener(this);
            snapToYItem = new JMenuItem("Y Axis");
            snapToYItem.addActionListener(this);
            snapToOriginItem = new JMenuItem("Origin");
            snapToOriginItem.addActionListener(this);
            snapSubMenu.add(snapToXItem);
            snapSubMenu.add(snapToYItem);
            snapSubMenu.add(snapToOriginItem);
            add(snapSubMenu);
        }
        
		@Override
		public void actionPerformed(ActionEvent e) {
			JMenuItem source = (JMenuItem)(e.getSource());
			if (source == snapToXItem) {
				double range = yMax - yMin;
				yMax = range / 2.0;
				yMin = range / -2.0;
				
				updateFunctions();
				plot.repaint();
			}
			else if (source == snapToYItem) {
				double domain = xMax - xMin;
				xMax = domain / 2.0;
				xMin = domain / -2.0;
				
				updateFunctions();
				plot.repaint();
			}
			else if (source == snapToOriginItem) {
				double domain = xMax - xMin;
				xMax = domain / 2.0;
				xMin = domain / -2.0;
				
				double range = yMax - yMin;
				yMax = range / 2.0;
				yMin = range / -2.0;
				
				updateFunctions();
				plot.repaint();
			}
		}
    }
    
    
    class GraphicsMouseAdapter extends MouseAdapter {
    	
    	private Plot2D plot;
    	
    	public GraphicsMouseAdapter(Plot2D plot) {
    		this.plot = plot;
    	}
    	
        @Override
        public void mousePressed(MouseEvent e) {
        	if (!perfOper && isCenterClick(e) && isOnGraph(e.getX(), e.getY())) {
                startPan(e);
            }
        }
        
        @Override
        public void mouseReleased(MouseEvent e) {
        	if (!perfOper && shouldShowMenu(e)) {
                doPopupMenu(e);
            }
            if (isCenterClick(e)) {
                endPan(e);
            }
        }
        
        public void mouseClicked(MouseEvent e) {
        	
        }
        
        @Override
        public void mouseDragged(MouseEvent e) {
            if (panning) {
                int diffX = e.getX() - panStartX;
                int diffY = e.getY() - panStartY;
                
                double xShift = -1.0 * ((double)diffX)/pixelsPerUnitX;
                double yShift = ((double)diffY)/pixelsPerUnitY;
                
                xMin = panStartXMin + xShift;
                xMax = panStartXMax + xShift;
                yMin = panStartYMin + yShift;
                yMax = panStartYMax + yShift;
                
                updateFunctions();
                repaint();
            }
        }
        
        private void doPopupMenu(MouseEvent e) {
            GraphRightClickMenu menu = new GraphRightClickMenu(plot);
            menu.show(e.getComponent(), e.getX(), e.getY());
        }
        
        private boolean shouldShowMenu(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON3) {
                int x = e.getX();
                int y = e.getY();
                return isOnGraph(x, y);
            }
            return false;
        }
        
        private boolean isCenterClick(MouseEvent e) {
            return e.getButton() == MouseEvent.BUTTON2;
        }
        
        private boolean isOnGraph(int x, int y) {
        	return (x > leftX && x < rightX && y < bottomY && y > topY);
        }
        
        private void startPan(MouseEvent e) {
            perfOper = true;
            panning = true;
            panStartX = e.getX();
            panStartY = e.getY();
            panStartXMin = xMin;
            panStartXMax = xMax;
            panStartYMin = yMin;
            panStartYMax = yMax;
        }
        
        private void endPan(MouseEvent e) {
            panning = false;
            perfOper = false;
        }
        
        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {
            int notches = e.getWheelRotation();
            
            double centerX = (xMax + xMin) / 2.0;
            double halfWidth = xMax - centerX;
            
            double centerY = (yMax + yMin) / 2.0;
            double halfHeight = yMax - centerY;
            
            if (notches > 0) {
                xMin = centerX - ((1.0 + (scrollOutSens * notches)) * halfWidth);
                xMax = centerX + ((1.0 + (scrollOutSens * notches)) * halfWidth);
                yMin = centerY - ((1.0 + (scrollOutSens * notches)) * halfHeight);
                yMax = centerY + ((1.0 + (scrollOutSens * notches)) * halfHeight);
            }
            else {
                xMin = centerX - ((1.0 + (scrollInSens * notches)) * halfWidth);
                xMax = centerX + ((1.0 + (scrollInSens * notches)) * halfWidth);
                yMin = centerY - ((1.0 + (scrollInSens * notches)) * halfHeight);
                yMax = centerY + ((1.0 + (scrollInSens * notches)) * halfHeight);
            }
            updateFunctions();
            repaint();
        }
        
        private boolean panning;
        private int panStartX;
        private int panStartY;
        private double panStartXMin;
        private double panStartXMax;
        private double panStartYMin;
        private double panStartYMax;
        
        private boolean perfOper = false; // Stores if we are currently performing an operation, such as zooming or panning
    }
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Enumerations -------------------------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    public enum FUNC_TYPE {
        POINT,
        LINE,
        POINT_AND_LINE
    }
    
}
